iT邦幫忙

2023 iThome 鐵人賽

DAY 2
1

第一天開始,首先我們來一起看看 Ktor 是怎麼開始一個 server 服務的。

要開始研究這個事情,首先我們要先看看 Ktor 框架內的程式碼怎麼撰寫。

我們透過 https://start.ktor.io/ 下載一個 Ktor 專案之後,可以看到預設的程式碼如下

fun main() {
    embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module)
        .start(wait = true)
}

今天我們先釐清一個問題:Ktor 這段程式碼,是怎麼開啟一個 server 服務的?

我們往下追 embeddedServer 的實作如下

@OptIn(DelicateCoroutinesApi::class)
public fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration>
embeddedServer(
    factory: ApplicationEngineFactory<TEngine, TConfiguration>,
    port: Int = 80,
    host: String = "0.0.0.0",
    watchPaths: List<String> = listOf(WORKING_DIRECTORY_PATH),
    configure: TConfiguration.() -> Unit = {},
    module: Application.() -> Unit
): TEngine = GlobalScope.embeddedServer(factory, port, host, watchPaths, EmptyCoroutineContext, configure, module)

這邊直接在 GlobalScope 這個 Kotlin 的 CoroutineScope 下面,呼叫了 embeddedServer(),我們繼續往下追:

public fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration>
    CoroutineScope.embeddedServer(
        factory: ApplicationEngineFactory<TEngine, TConfiguration>,
        vararg connectors: EngineConnectorConfig = arrayOf(EngineConnectorBuilder()),
        watchPaths: List<String> = listOf(WORKING_DIRECTORY_PATH),
        parentCoroutineContext: CoroutineContext = EmptyCoroutineContext,
        configure: TConfiguration.() -> Unit = {},
        module: Application.() -> Unit
    ): TEngine {
    val environment = applicationEngineEnvironment {
        this.parentCoroutineContext = coroutineContext + parentCoroutineContext
        this.log = KtorSimpleLogger("ktor.application")
        this.watchPaths = watchPaths
        this.module(module)
        this.connectors.addAll(connectors)
    }

    return embeddedServer(factory, environment, configure)
}

這邊可以看到,這個對 CoroutineScope 所定義的延伸函數(extension function)只做了一件事情:設置 server 所需要的環境,包含 logger、connectors 等等。剩下的部分直接交給了對應的 factory 進行處理。

再往下追,我們會看到

public fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration> embeddedServer(
    factory: ApplicationEngineFactory<TEngine, TConfiguration>,
    environment: ApplicationEngineEnvironment,
    configure: TConfiguration.() -> Unit = {}
): TEngine {
    return factory.create(environment, configure)
}

也就是說,如果要找到實際產生 server 的程式碼,我們就要看 embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module) 這段裡面的 Netty 是怎麼實作 create()這個函數的。

我們開始看 Netty 的實作,這是一個 Kotlin 的 object:

public object Netty : ApplicationEngineFactory<NettyApplicationEngine, NettyApplicationEngine.Configuration> {
    override fun create(
        environment: ApplicationEngineEnvironment,
        configure: NettyApplicationEngine.Configuration.() -> Unit
    ): NettyApplicationEngine {
        return NettyApplicationEngine(environment, configure)
    }
}

到這邊,我們終於抽絲剝繭,挖出了最終會拿到的物件類別:NettyApplicationEngine

另外我們在這邊也看到了一個設計模式:工廠模式。

透過定義 ApplicationEngineFactory 這個介面,我們就可以簡單地切換送入 embeddedServer 函式的參數,只要我們傳送進去的 Factory,最終能 create()ApplicationEngine,我們就可以透過切換 Factory,來取得不同的 ApplicationEngine

以這裡來說,我們拿到的會是一個 NettyApplicationEngine

/**
 * [ApplicationEngine] implementation for running in a standalone Netty
 */
 public class NettyApplicationEngine(
    environment: ApplicationEngineEnvironment,
    configure: Configuration.() -> Unit = {}
) : BaseApplicationEngine(environment) 

到這邊,我們終於看到了運作一個獨立 Netty 的部分了!

我們來一起看看他對 start()的實作,這段比較多,我們分段落來看:

    override fun start(wait: Boolean): NettyApplicationEngine {
        addShutdownHook {
            stop(configuration.shutdownGracePeriod, configuration.shutdownTimeout)
        }

        environment.start()

前面加上了一個 addShutdownHook ,從命名和註解,我們可以得知是加上了 JVM shutdown 時該做的事情:

/**
 * Adds automatic JVM shutdown hooks management. Should be used **before** starting the engine.
 * Once JVM termination noticed, [stop] block will be executed.
 * Please note that a shutdown hook only registered when the application is running. If the application
 * is already stopped then there will be no hook and no [stop] function invocation possible.
 * So [stop] block will be called once or never.
 */

之後呼叫了 environment.start(),我們看看 environment.start() 上面的註解

/**
 * Starts [ApplicationEngineEnvironment] and creates an application.
 */
public fun start()

這個函數的註解,就很有意思了!原來 environment.start() 除了啟動 ApplicationEngineEnvironment,還會在這邊建立 application。

(p.s. 所以不要再說,不需要註解了!!)

建立 application 之後,看到下面這段

try {
	channels = bootstraps.zip(environment.connectors)
		.map { it.first.bind(it.second.host, it.second.port) }
		.map { it.sync().channel() }
	val connectors = channels!!.zip(environment.connectors)
		.map { it.second.withPort(it.first.localAddress().port) }
	resolvedConnectors.complete(connectors)
} catch (cause: BindException) {
	terminate()
	throw cause
}

這段的寫法是利用了 Kotlin Collection 的寫法,很簡短地將 bootstrapsenvironment.connectors 進行綁定,如果綁定失敗就拋出 BindException

最後,我們加上一些監控和錯誤處理的部分,start() 就結束了


        environment.monitor.raiseCatching(ServerReady, environment, environment.log)

        cancellationDeferred =
            stopServerOnCancellation(configuration.shutdownGracePeriod, configuration.shutdownTimeout)

        if (wait) {
            channels?.map { it.closeFuture() }?.forEach { it.sync() }
            stop(configuration.shutdownGracePeriod, configuration.shutdownTimeout)
        }
        return this
	}

到這邊,我們就看完了 Ktor 針對 embeddedServer() 的實作、知道了 Netty 的產出,也看了 NettyApplicationEnginestart() 的實作。

今天我們就先到這邊為止,各位明天見!


上一篇
Day 01:不是寫 Kotlin,而是讀 Kotlin
下一篇
Day 03:設置路由 `Application.configureRouting()`
系列文
深入解析 Kotlin 專案 Ktor 的程式碼,探索 Ktor 的強大功能30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言